// ##########################################################################
// #                                                                        #
// #                              CLOUDCOMPARE                              #
// #                                                                        #
// #  This program is free software; you can redistribute it and/or modify  #
// #  it under the terms of the GNU General Public License as published by  #
// #  the Free Software Foundation; version 2 or later of the License.      #
// #                                                                        #
// #  This program is distributed in the hope that it will be useful,       #
// #  but WITHOUT ANY WARRANTY; without even the implied warranty of        #
// #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the          #
// #  GNU General Public License for more details.                          #
// #                                                                        #
// #          COPYRIGHT: EDF R&D / TELECOM ParisTech (ENST-TSI)             #
// #                                                                        #
// ##########################################################################

// GUIs generated by Qt Designer
#include <ui_openAsciiFileDlg.h>

// Local
#include "AsciiOpenDlg.h"
#include "FileIOFilter.h"

// qCC_db
#include <ccPointCloud.h>

// Qt
#include <QComboBox>
#include <QDesktopWidget>
#include <QDialogButtonBox>
#include <QFile>
#include <QLineEdit>
#include <QMessageBox>
#include <QPushButton>
#include <QSpinBox>
#include <QTableWidget>
#include <QTableWidgetItem>
#include <QTextStream>
#include <QToolButton>

// system
#include <cassert>
#include <cstdio>
#include <cstring>

// Semi-persistent value for max. cloud size
static double s_maxCloudSizeDoubleSpinBoxValue = (CC_MAX_NUMBER_OF_POINTS_PER_CLOUD / 1.0e6);
// Semi-persistent value for C.S. entities (quaternion) scale
static double s_csEntitiesScale = 1.0;

// Max number of points/lines to detect a column as 'labels'
static unsigned s_maxLabelCount = 256;

//! Dialog 'context'
struct AsciiOpenContext
{
	//! Default initializer
	AsciiOpenContext()
	    : separator(' ')
	    , extractSFNameFrom1stLine(false)
	    , maxPointCountPerCloud(0)
	    , skipLines(0)
	    , applyAll(false)
	    , commaDecimal(false)
	{
	}

	//! Saves state
	void save(Ui_AsciiOpenDialog* ui)
	{
		extractSFNameFrom1stLine = ui->extractSFNamesFrom1stLineCheckBox->isChecked();
		maxPointCountPerCloud    = ui->maxCloudSizeDoubleSpinBox->value();
		separator                = ui->lineEditSeparator->text().at(0);
		skipLines                = ui->spinBoxSkipLines->value();
		commaDecimal             = ui->commaDecimalCheckBox->isChecked();
	}

	//! Restores state
	void load(Ui_AsciiOpenDialog* ui) const
	{
		ui->maxCloudSizeDoubleSpinBox->setValue(maxPointCountPerCloud);
		ui->lineEditSeparator->blockSignals(true);
		ui->lineEditSeparator->setText(separator);
		ui->lineEditSeparator->blockSignals(false);
		ui->spinBoxSkipLines->blockSignals(true);
		ui->spinBoxSkipLines->setValue(skipLines);
		ui->spinBoxSkipLines->blockSignals(false);
		ui->extractSFNamesFrom1stLineCheckBox->setChecked(extractSFNameFrom1stLine);
		ui->extractSFNamesFrom1stLineCheckBox->setEnabled(skipLines > 0);
		ui->commaDecimalCheckBox->blockSignals(true);
		ui->commaDecimalCheckBox->setChecked(commaDecimal);
		ui->commaDecimalCheckBox->setEnabled(separator != ',');
		ui->commaDecimalCheckBox->blockSignals(false);
	}

	AsciiOpenDlg::Sequence sequence;
	QChar                  separator;
	bool                   extractSFNameFrom1stLine;
	double                 maxPointCountPerCloud;
	int                    skipLines;
	bool                   applyAll;
	bool                   commaDecimal;
};

//! Semi-persistent loading context
static QScopedPointer<AsciiOpenContext> s_asciiOpenContext;

AsciiOpenDlg::AsciiOpenDlg(QWidget* parent)
    : QDialog(parent)
    , m_ui(new Ui_AsciiOpenDialog)
    , m_separator(' ')
    , m_averageLineSize(-1.0)
    , m_stream(nullptr)
    , m_columnsCount(0)
{
	m_ui->setupUi(this);

	m_ui->commentLinesSkippedLabel->hide();

	connect(m_ui->applyButton, &QPushButton::clicked, this, &AsciiOpenDlg::apply);
	connect(m_ui->applyAllButton, &QPushButton::clicked, this, &AsciiOpenDlg::applyAll);
	connect(m_ui->cancelButton, &QPushButton::clicked, this, &AsciiOpenDlg::reject);
	connect(m_ui->lineEditSeparator, &QLineEdit::textChanged, this, &AsciiOpenDlg::onSeparatorChange);
	connect(m_ui->commaDecimalCheckBox, &QCheckBox::toggled, this, &AsciiOpenDlg::commaDecimalCheckBoxToggled);
	connect(m_ui->spinBoxSkipLines, qOverload<int>(&QSpinBox::valueChanged), this, &AsciiOpenDlg::onSkippedLinesChanged);
	connect(m_ui->resetColumnsToolButton, &QToolButton::clicked, this, &AsciiOpenDlg::resetColumnRoles);

	// shortcut buttons
	connect(m_ui->toolButtonShortcutSpace, &QToolButton::clicked, this, &AsciiOpenDlg::shortcutButtonPressed);
	connect(m_ui->toolButtonShortcutComma, &QToolButton::clicked, this, &AsciiOpenDlg::shortcutButtonPressed);
	connect(m_ui->toolButtonShortcutSemicolon, &QToolButton::clicked, this, &AsciiOpenDlg::shortcutButtonPressed);

	m_ui->maxCloudSizeDoubleSpinBox->setMaximum(CC_MAX_NUMBER_OF_POINTS_PER_CLOUD / 1.0e6);
	m_ui->maxCloudSizeDoubleSpinBox->setValue(s_maxCloudSizeDoubleSpinBoxValue);

	m_ui->quaternionFrame->setVisible(false);
	m_ui->quatCSScaleDoubleSpinBox->setValue(s_csEntitiesScale);

	QSize screenSize = QApplication::desktop()->screenGeometry().size();
	setMaximumSize(screenSize);
}

AsciiOpenDlg::~AsciiOpenDlg()
{
	delete m_ui;
	m_ui = nullptr;
}

bool AsciiOpenDlg::setInput(const QString& filename, QTextStream* stream /*=nullptr*/)
{
	m_filename = filename;
	m_stream   = stream;

	// update title
	m_ui->lineEditFileName->setText(m_filename);

	// try to restore the previous context
	if (s_asciiOpenContext && restorePreviousContext())
	{
		return s_asciiOpenContext->applyAll;
	}

	// else try to find the file format automatically
	autoFindBestSeparator();

	return false; // the current configuration shouldn't be applied automatically ('apply all' is false, or the configuration is different from the previous one)
}

void AsciiOpenDlg::autoFindBestSeparator()
{
	const QList<QChar> separators{QChar(' '),
	                              QChar(','),
	                              QChar(';')};

	// We try all default separators...
	size_t maxValidColumnCount = 0;
	QChar  bestSep             = separators.front();
	for (QChar sep : separators)
	{
		setSeparator(sep); // this eventually calls 'updateTable'

		size_t validColumnCount = 0;
		for (ColumnType type : m_columnType)
		{
			if (type != TEXT)
			{
				++validColumnCount;
			}
		}

		//...and look for the separator that yields the best results
		if (validColumnCount > maxValidColumnCount)
		{
			maxValidColumnCount = validColumnCount;
			bestSep             = sep;
		}
	}

	setSeparator(bestSep); // this eventually calls 'updateTable'
}

void AsciiOpenDlg::onSkippedLinesChanged(int lineCount)
{
	if (lineCount < 0)
	{
		assert(false);
		return;
	}

	updateTable();
}

static bool CouldBeX(const QString& colHeader)
{
	return colHeader.startsWith(AsciiHeaderColumns::X().toUpper()) || colHeader == "TX";
}
static bool CouldBeY(const QString& colHeader)
{
	return colHeader.startsWith(AsciiHeaderColumns::Y().toUpper()) || colHeader == "TY";
}
static bool CouldBeZ(const QString& colHeader)
{
	return colHeader.startsWith(AsciiHeaderColumns::Z().toUpper()) || colHeader == "TZ";
}
static bool CouldBeRf(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::Rf().toUpper();
}
static bool CouldBeGf(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::Gf().toUpper();
}
static bool CouldBeBf(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::Bf().toUpper();
}
static bool CouldBeAf(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::Af().toUpper();
}
static bool CouldBeR(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::R().toUpper() || colHeader.contains("RED");
}
static bool CouldBeG(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::G().toUpper() || colHeader.contains("GREEN");
}
static bool CouldBeB(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::B().toUpper() || colHeader.contains("BLUE");
}
static bool CouldBeA(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::A().toUpper() || colHeader.contains("ALPHA");
}
static bool CouldBeNx(const QString& colHeader)
{
	return colHeader.startsWith(AsciiHeaderColumns::Nx().toUpper()) || (colHeader.contains("NORM") && colHeader.contains("X"));
}
static bool CouldBeNy(const QString& colHeader)
{
	return colHeader.startsWith(AsciiHeaderColumns::Ny().toUpper()) || (colHeader.contains("NORM") && colHeader.contains("Y"));
}
static bool CouldBeNz(const QString& colHeader)
{
	return colHeader.startsWith(AsciiHeaderColumns::Nz().toUpper()) || (colHeader.contains("NORM") && colHeader.contains("Z"));
}

static bool CouldBeGrey(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::Grey().toUpper();
}
static bool CouldBeRGBi(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::RGB32i().toUpper();
}
static bool CouldBeRGBf(const QString& colHeader)
{
	return colHeader == AsciiHeaderColumns::RGB32f().toUpper();
}
static bool CouldBeScal(const QString& colHeader)
{
	return colHeader.contains("SCALAR") || colHeader.contains("INTENSITY");
}
static bool CouldBeQuatW(const QString& colHeader)
{
	return colHeader.startsWith(AsciiHeaderColumns::QuatX().toUpper()) || colHeader == "QW" || colHeader == "Q0";
}
static bool CouldBeQuatX(const QString& colHeader)
{
	return colHeader.startsWith(AsciiHeaderColumns::QuatY().toUpper()) || colHeader == "QX";
}
static bool CouldBeQuatY(const QString& colHeader)
{
	return colHeader.startsWith(AsciiHeaderColumns::QuatZ().toUpper()) || colHeader == "QY";
}
static bool CouldBeQuatZ(const QString& colHeader)
{
	return colHeader.startsWith(AsciiHeaderColumns::QuatW().toUpper()) || colHeader == "QZ";
}
static bool CouldBeLabel(const QString& colHeader)
{
	return colHeader.contains("LABEL") || colHeader.contains("NAME");
}

static const unsigned MAX_COLUMNS          = 512; // maximum number of columns that can be handled
static const unsigned LINES_READ_FOR_STATS = 200; // number of lines read for stats
static const unsigned DISPLAYED_LINES      = 20;  // number of displayed lines

static unsigned X_BIT     = 1;
static unsigned Y_BIT     = 2;
static unsigned Z_BIT     = 4;
static unsigned W_BIT     = 8;
static unsigned XYZ_BITS  = X_BIT | Y_BIT | Z_BIT;
static unsigned XYZW_BITS = XYZ_BITS | W_BIT;

static int EnabledBits(unsigned bitField)
{
	int count = 0;
	if (bitField & X_BIT)
		++count;
	if (bitField & Y_BIT)
		++count;
	if (bitField & Z_BIT)
		++count;
	if (bitField & W_BIT)
		++count;

	return count;
}

void AsciiOpenDlg::onSeparatorChange(const QString& separator)
{
	assert(separator.size() == 1);
	if (separator.length() < 1)
	{
		m_ui->asciiCodeLabel->setText("Enter a valid character!");
		m_ui->buttonWidget->setEnabled(false);
		m_ui->tableWidget->clear();
		m_columnType.clear();
		return;
	}

	// new separator
	m_separator = separator[0];
	m_ui->asciiCodeLabel->setText(QString("(ASCII code: %1)").arg(m_separator.unicode()));

	m_headerLine.clear(); // to force re-assignation of columns!
	m_columnType.clear();

	updateTable();
}

bool AsciiOpenDlg::useCommaAsDecimal() const
{
	return m_ui->commaDecimalCheckBox->isEnabled() && m_ui->commaDecimalCheckBox->isChecked();
}

void AsciiOpenDlg::commaDecimalCheckBoxToggled(bool)
{
	onSeparatorChange(m_ui->lineEditSeparator->text());
}

void AsciiOpenDlg::updateTable()
{
	m_ui->tableWidget->setEnabled(false);
	m_ui->extractSFNamesFrom1stLineCheckBox->setEnabled(false);

	bool hadValidHeader = !m_headerLine.isEmpty();
	m_headerLine.clear();
	m_ui->headerLabel->setVisible(false);

	if (m_filename.isEmpty() && m_stream == nullptr)
	{
		m_ui->tableWidget->clear();
		return;
	}

	// open the file
	QFile file;
	if (nullptr == m_stream)
	{
		file.setFileName(m_filename);
		if (!file.open(QFile::ReadOnly))
		{
			m_ui->tableWidget->clear();
			m_columnType.clear();
			return;
		}
		m_stream = new QTextStream(&file);
	}
	assert(m_stream);
	m_stream->seek(0);

	// we skip the first lines (if requested)
	{
		for (int i = 0; i < m_ui->spinBoxSkipLines->value();)
		{
			QString currentLine = m_stream->readLine();
			if (currentLine.isNull())
			{
				// end of file reached
				assert(m_stream->atEnd());
				break;
			}
			if (currentLine.isEmpty())
			{
				// empty lines are simply ignored
				continue;
			}
			// we keep track of the first skipped line (= potential header)
			if (i == 0)
			{
				m_headerLine = currentLine;
			}
			++i;
		}
	}

	// if the old setup has less than 3 columns, we forget it
	if (m_columnsCount < 3)
	{
		m_ui->tableWidget->clear();
		m_columnType.clear();
		m_columnsCount = 0;
	}
	m_ui->tableWidget->setRowCount(DISPLAYED_LINES + 1); //+1 for first line shifting

	unsigned lineCount    = 0; // number of lines read
	unsigned totalChars   = 0; // total read characters (for stats)
	unsigned columnsCount = 0; // max columns count per line
	unsigned commentLines = 0; // number of comments line skipped

	std::vector<bool> valueIsNumber;   // identifies columns with numbers only [mandatory]
	std::vector<bool> valueIsBelowOne; // identifies columns with values between -1 and 1 only
	std::vector<bool> valueIsInteger;  // identifies columns with integer values only
	std::vector<bool> valueIsBelow255; // identifies columns with integer values between 0 and 255 only

	bool    commaAsDecimal = useCommaAsDecimal();
	QLocale locale(commaAsDecimal ? QLocale::French : QLocale::English);
	QChar   decimalPoint = commaAsDecimal ? ',' : '.';
	while (lineCount < LINES_READ_FOR_STATS)
	{
		QString currentLine = m_stream->readLine();
		if (currentLine.isNull())
		{
			// end of file reached
			break;
		}
		if (currentLine.isEmpty())
		{
			// empty lines are ignored
			continue;
		}

		// we recognize "//" as the beginning of a comment
		if (!currentLine.startsWith("//") /* || !currentLine.startsWith("#")*/)
		{
			QStringList parts = currentLine.simplified().split(m_separator);
			if (lineCount < DISPLAYED_LINES)
			{
				unsigned partsCount              = std::min(MAX_COLUMNS, static_cast<unsigned>(parts.size()));
				bool     columnCountHasIncreased = (partsCount > columnsCount);

				// do we need to add one or several new columns?
				if (columnCountHasIncreased)
				{
					// we also extend vectors
					for (unsigned i = columnsCount; i < partsCount; ++i)
					{
						valueIsNumber.push_back(true);
						valueIsBelowOne.push_back(true);
						valueIsBelow255.push_back(true);
						valueIsInteger.push_back(true);
					}

					if (m_ui->tableWidget->columnCount() < static_cast<int>(partsCount))
					{
						// DGM: at this stage we must not reduce the table!
						// The first line is sometimes smaller than the next ones
						// and we want to keep the widgets/configuration for the
						// other columns!
						m_ui->tableWidget->setColumnCount(partsCount);
					}
					else if (m_ui->tableWidget->columnCount() > static_cast<int>(partsCount))
					{
						// remove the unnecessary cells!
						for (int i = static_cast<int>(partsCount); i < m_ui->tableWidget->columnCount(); ++i)
						{
							m_ui->tableWidget->setItem(lineCount + 1, i, nullptr);
						}
					}
					columnsCount = partsCount;
				}

				// we fill the current row with extracted parts
				for (unsigned i = 0; i < partsCount; ++i)
				{
					QTableWidgetItem* newItem = new QTableWidgetItem(parts[i]);

					// test values
					bool   isANumber = false;
					double value     = locale.toDouble(parts[i], &isANumber);
					if (!isANumber)
					{
						valueIsNumber[i]   = false;
						valueIsBelowOne[i] = false;
						valueIsInteger[i]  = false;
						valueIsBelow255[i] = false;
						newItem->setBackground(QBrush(QColor(255, 160, 160)));
					}
					else
					{
						if (columnCountHasIncreased || (lineCount == 1 && !valueIsNumber[i]))
						{
							// the previous lines were probably header lines
							// we can forget about their content otherwise it will prevent us from detecting the right pattern
							valueIsNumber[i]   = true;
							valueIsBelowOne[i] = true;
							valueIsInteger[i]  = true;
							valueIsBelow255[i] = true;
						}
						valueIsBelowOne[i] = valueIsBelowOne[i] && (std::abs(value) <= 1.0);
						valueIsInteger[i]  = valueIsInteger[i] && !parts[i].contains(decimalPoint);
						valueIsBelow255[i] = valueIsBelow255[i] && valueIsInteger[i] && (value >= 0.0 && value <= 255.0);
					}

					m_ui->tableWidget->setItem(lineCount + 1, i, newItem); //+1 for first line shifting
				}
			}

			totalChars += currentLine.size() + 1; //+1 for return char at eol
			++lineCount;
		}
		else
		{
			if (m_ui->spinBoxSkipLines->value() == 0 && commentLines == 0)
			{
				// if the very first line is a comment, then we automatically skip it!
				// this way it will be considered as a header
				m_headerLine = currentLine;
				setSkippedLines(1, true);
			}
			++commentLines;
		}
	}

	file.close();

	// now we can reduce the table (if necessary)
	if (m_ui->tableWidget->columnCount() > static_cast<int>(columnsCount))
	{
		m_ui->tableWidget->setColumnCount(columnsCount);
	}

	// process header line
	if (!m_headerLine.isEmpty())
	{
		m_headerLine = m_headerLine.trimmed();
		int n        = 0;
		while (n < m_headerLine.size() && m_headerLine.at(n) == '/')
		{
			++n;
		}
		if (n != 0)
		{
			m_headerLine.remove(0, n);
		}

		QString displayHeader = m_headerLine;
		if (m_headerLine.length() > 256)
		{
			displayHeader = m_headerLine.left(256) + "...";
		}
		m_ui->headerLabel->setText(QString("Header: ") + displayHeader);
		m_ui->headerLabel->setVisible(true);
		m_ui->extractSFNamesFrom1stLineCheckBox->setEnabled(true);
	}

	if (commentLines)
	{
		m_ui->commentLinesSkippedLabel->setText(QString("+ %1 comment line(s) skipped").arg(commentLines));
		m_ui->commentLinesSkippedLabel->setVisible(true);
	}

	if (lineCount == 0 || columnsCount == 0)
	{
		m_averageLineSize = -1.0;
		m_ui->tableWidget->clear();
		m_columnType.clear();
		return;
	}

	// average line size
	m_averageLineSize                  = static_cast<double>(totalChars) / lineCount;
	unsigned approximateTotalLineCount = static_cast<unsigned>(file.size() / m_averageLineSize);

	// we add a type selector for each column
	QStringList propsText;
	{
		propsText.reserve(ASCII_OPEN_DLG_TYPES_COUNT);
		for (unsigned i = 0; i < ASCII_OPEN_DLG_TYPES_COUNT; i++)
		{
			propsText << QString(ASCII_OPEN_DLG_TYPES_NAMES[i]);
		}
	}

	// remove unnecessary columns
	{
		while (columnsCount < m_columnsCount)
			m_ui->tableWidget->removeColumn(--m_columnsCount);
		if (m_columnType.size() > columnsCount)
			m_columnType.resize(columnsCount, UNKNOWN);
		for (unsigned i = lineCount + 1; i <= DISPLAYED_LINES; ++i)
			m_ui->tableWidget->removeRow(i);
	}

	// setup table and widgets
	{
		// Icons
		static const QIcon xIcon(QString::fromUtf8(":/CC/images/typeXCoordinate.png"));
		static const QIcon yIcon(QString::fromUtf8(":/CC/images/typeYCoordinate.png"));
		static const QIcon zIcon(QString::fromUtf8(":/CC/images/typeZCoordinate.png"));
		static const QIcon NormIcon(QString::fromUtf8(":/CC/images/typeNormal.png"));
		static const QIcon RGBIcon(QString::fromUtf8(":/CC/images/typeRgbCcolor.png"));
		static const QIcon GreyIcon(QString::fromUtf8(":/CC/images/typeGrayColor.png"));
		static const QIcon ScalarIcon(QString::fromUtf8(":/CC/images/typeSF.png"));
		static const QIcon LabelIcon(QString::fromUtf8(":/CC/images/dbLabelSymbol.png"));
		static const QIcon QuatIcon(QString::fromUtf8(":/CC/images/typeQuaternion.png"));

		int columnWidth = (m_ui->tableWidget->width() * 9) / (columnsCount * 10);
		columnWidth     = std::max(columnWidth, 80);

		for (unsigned i = 0; i < columnsCount; i++)
		{
			QComboBox* columnHeaderWidget = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i));
			QComboBox* _columnHeader      = columnHeaderWidget;
			if (!columnHeaderWidget)
			{
				columnHeaderWidget = new QComboBox();
				columnHeaderWidget->addItems(propsText);
				columnHeaderWidget->setMaxVisibleItems(ASCII_OPEN_DLG_TYPES_COUNT);
				columnHeaderWidget->setCurrentIndex(0);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_X, xIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_Y, yIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_Z, zIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_NX, NormIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_NY, NormIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_NZ, NormIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_R, RGBIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_G, RGBIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_B, RGBIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_A, RGBIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_Rf, RGBIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_Gf, RGBIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_Bf, RGBIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_Af, RGBIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_Grey, GreyIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_RGB32i, RGBIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_RGB32f, RGBIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_Label, LabelIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_QuatW, QuatIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_QuatX, QuatIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_QuatY, QuatIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_QuatZ, QuatIcon);
				columnHeaderWidget->setItemIcon(ASCII_OPEN_DLG_Scalar, ScalarIcon);

				connect(columnHeaderWidget, qOverload<int>(&QComboBox::currentIndexChanged), this, &AsciiOpenDlg::columnsTypeHasChanged);
			}

			while (m_columnType.size() <= static_cast<size_t>(i))
				m_columnType.push_back(UNKNOWN);
			assert(m_columnType.size() >= static_cast<size_t>(i));

			if (!_columnHeader)
				m_ui->tableWidget->setCellWidget(0, i, columnHeaderWidget);
			m_ui->tableWidget->setColumnWidth(i, columnWidth);

			// a non-numerical column can't be valid
			if (!valueIsNumber[i])
			{
				m_columnType[i] = TEXT;
			}
			else
			{
				// we must do this to ensure we can get a correct result.
				// Otherwise, we may fail in such situations:
				//
				// FILE:
				// Line1   : $ gps file
				// Line2   : $ id name x y z
				// Line3   : 500
				// Line4   : 0    0001.JPG  753811.417453   4307200.381522      1957.803955
				// Linex   : ......
				// Line503 : 499  0500.JPG  753630.672714   4307195.433217      1957.803955
				//
				// Description:
				// once we open the file, we will get a %m_columnType with 5 values of "TEXT"
				// then if we choose to skip the 3 first lines, we get a %valueIsNumber with 5 "true"
				// but the %m_columnType is still with 5 values of "TEXT" which leads to the failure!
				m_columnType[i] = UNKNOWN;
			}
		}
	}

	// auto-detect columns 'roles'
	{
		// DGM: bit flags now
		unsigned assignedXYZFlags        = 0;
		unsigned assignedNormFlags       = 0;
		unsigned assignedRGBFlags        = 0;
		unsigned assignedQuaternionFlags = 0;

		// split header (if any)
		QStringList headerParts = m_headerLine.simplified().split(m_separator);
		bool        validHeader = (headerParts.size() >= static_cast<int>(columnsCount));
		m_ui->extractSFNamesFrom1stLineCheckBox->setEnabled(validHeader); // can we consider the first ignored line as a header?
		if (!validHeader)
		{
			// no need to keep it (+ it will serve as flag later)
			m_headerLine.clear();
		}
		else if (!hadValidHeader)
		{
			m_ui->extractSFNamesFrom1stLineCheckBox->setChecked(true);
			for (ColumnType& type : m_columnType)
			{
				if (type != TEXT)
					type = UNKNOWN;
			}
			// std::fill(m_columnType.begin(), m_columnType.end(), UNKNOWN); //if the header has changed, we force update of all columns!
		}

		// first with the help of the header (if any)
		bool labelColumnAssigned = false;
		if (validHeader)
		{
			for (unsigned i = 0; i < columnsCount; i++)
			{
				// we try to guess columns if we have a valid header for the first time!
				if (m_columnType[i] == UNKNOWN || m_columnType[i] == IGNORED)
				{
					QComboBox* columnHeaderWidget = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i));
					assert(columnHeaderWidget);

					columnHeaderWidget->blockSignals(true);

					QString colHeader = headerParts[i].toUpper();

					if ((assignedXYZFlags & X_BIT) == 0 && CouldBeX(colHeader))
					{
						// X
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_X);
						assignedXYZFlags |= X_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedXYZFlags & Y_BIT) == 0 && CouldBeY(colHeader))
					{
						// Y
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Y);
						assignedXYZFlags |= Y_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedXYZFlags & Z_BIT) == 0 && CouldBeZ(colHeader))
					{
						// Z
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Z);
						assignedXYZFlags |= Z_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedRGBFlags & X_BIT) == 0 && CouldBeRf(colHeader))
					{
						// Red
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Rf);
						assignedRGBFlags |= X_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedRGBFlags & Y_BIT) == 0 && CouldBeGf(colHeader))
					{
						// Green
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Gf);
						assignedRGBFlags |= Y_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedRGBFlags & Z_BIT) == 0 && CouldBeBf(colHeader))
					{
						// Blue
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Bf);
						assignedRGBFlags |= Z_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedRGBFlags & X_BIT) == 0 && CouldBeR(colHeader))
					{
						// Red
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_R);
						assignedRGBFlags |= X_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedRGBFlags & Y_BIT) == 0 && CouldBeG(colHeader))
					{
						// Green
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_G);
						assignedRGBFlags |= Y_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedRGBFlags & Z_BIT) == 0 && CouldBeB(colHeader))
					{
						// Blue
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_B);
						assignedRGBFlags |= Z_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedRGBFlags & W_BIT) == 0 && CouldBeA(colHeader))
					{
						// Blue
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_A);
						assignedRGBFlags |= W_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedNormFlags & X_BIT) == 0 && CouldBeNx(colHeader))
					{
						// Nx
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_NX);
						assignedNormFlags |= X_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedNormFlags & Y_BIT) == 0 && CouldBeNy(colHeader))
					{
						// Ny
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_NY);
						assignedNormFlags |= Y_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedNormFlags & Z_BIT) == 0 && CouldBeNz(colHeader))
					{
						// Nz
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_NZ);
						assignedNormFlags |= Z_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedQuaternionFlags & X_BIT) == 0 && CouldBeQuatX(colHeader))
					{
						// Qx
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_QuatX);
						assignedQuaternionFlags |= X_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedQuaternionFlags & Y_BIT) == 0 && CouldBeQuatY(colHeader))
					{
						// Qy
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_QuatY);
						assignedQuaternionFlags |= Y_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedQuaternionFlags & Z_BIT) == 0 && CouldBeQuatZ(colHeader))
					{
						// Qz
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_QuatZ);
						assignedQuaternionFlags |= Z_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if ((assignedQuaternionFlags & W_BIT) == 0 && CouldBeQuatW(colHeader))
					{
						// QW
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_QuatW);
						assignedQuaternionFlags |= W_BIT; // update bit field accordingly
						m_columnType[i] = VALID;
					}
					else if (CouldBeGrey(colHeader))
					{
						// Intensity
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Grey);
						m_columnType[i] = VALID;
					}
					else if (CouldBeRGBi(colHeader))
					{
						// RGBi
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_RGB32i);
						m_columnType[i] = VALID;
					}
					else if (CouldBeRGBf(colHeader))
					{
						// RGBf
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_RGB32f);
						m_columnType[i] = VALID;
					}
					else if (CouldBeScal(colHeader))
					{
						// scalar field
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Scalar);
						m_columnType[i] = VALID;
					}
					else if (!labelColumnAssigned && approximateTotalLineCount < s_maxLabelCount && CouldBeLabel(colHeader)) // no need to promote labels if there are too many of them ;)
					{
						// label
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Label);
						m_columnType[i]     = VALID;
						labelColumnAssigned = true;
					}

					columnHeaderWidget->blockSignals(false);
				}
			}
		} // if (validHeader)

		// now for the auto-detection whithout header
		{
			for (unsigned i = 0; i < columnsCount; i++)
			{
				if (m_columnType[i] == TEXT)
				{
					if (!labelColumnAssigned && columnsCount > 1 && approximateTotalLineCount < s_maxLabelCount)
					{
						QComboBox* columnHeaderWidget = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i));
						assert(columnHeaderWidget);
						columnHeaderWidget->blockSignals(true);
						columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Label);
						columnHeaderWidget->blockSignals(false);

						labelColumnAssigned = true; // There Can Be Only One!
					}
					continue;
				}

				// now we deal with numerical values only
				assert(valueIsNumber[i]);

				// first time? let's try to assign each column a type
				if (m_columnType[i] == UNKNOWN && columnsCount > 1)
				{
					QComboBox* columnHeaderWidget = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i));
					assert(columnHeaderWidget);
					columnHeaderWidget->blockSignals(true);
					columnHeaderWidget->setCurrentIndex(-1);

					// by default, we assume that the first columns are always X,Y and Z
					if (assignedXYZFlags < XYZ_BITS)
					{
						// in rare cases, the first column is an index
						if (columnsCount > 3
						    && i == 0
						    && (EnabledBits(assignedXYZFlags) == 0)
						    && valueIsInteger[i]
						    && i + 1 < columnsCount
						    && !valueIsInteger[i + 1])
						{
							// let's consider it as a scalar
							columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Scalar);
						}
						else
						{
							if (!(assignedXYZFlags & X_BIT))
							{
								columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_X);
								assignedXYZFlags |= X_BIT; // update bit field accordingly
							}
							else if (!(assignedXYZFlags & Y_BIT))
							{
								columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Y);
								assignedXYZFlags |= Y_BIT; // update bit field accordingly
							}
							else if (!(assignedXYZFlags & Z_BIT))
							{
								columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Z);
								assignedXYZFlags |= Z_BIT; // update bit field accordingly
							}
						}
					}
					else
					{
						// looks like RGB?
						if (valueIsBelow255[i]
						    && assignedRGBFlags < XYZ_BITS
						    && (i + 2 - EnabledBits(assignedRGBFlags)) < columnsCount                                    // make sure that we can put all values there!
						    && (EnabledBits(assignedRGBFlags) > 0 || (valueIsBelow255[i + 1] && valueIsBelow255[i + 2])) // make sure that next values are also ok!
						)
						{
							if (!(assignedRGBFlags & X_BIT))
							{
								columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_R);
								assignedRGBFlags |= X_BIT; // update bit field accordingly
							}
							else if (!(assignedRGBFlags & Y_BIT))
							{
								columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_G);
								assignedRGBFlags |= Y_BIT; // update bit field accordingly
							}
							else if (!(assignedRGBFlags & Z_BIT))
							{
								columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_B);
								assignedRGBFlags |= Z_BIT; // update bit field accordingly
							}
							else if (!(assignedRGBFlags & W_BIT))
							{
								columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_A);
								assignedRGBFlags |= W_BIT; // update bit field accordingly
							}
						}
						// looks like a normal vector?
						else if (valueIsBelowOne[i]
						         && assignedNormFlags < XYZ_BITS
						         && (i + 2 - EnabledBits(assignedNormFlags) < columnsCount)                                    // make sure that we can put all values there!
						         && (EnabledBits(assignedNormFlags) > 0 || (valueIsBelowOne[i + 1] && valueIsBelowOne[i + 2])) // make sure that next values are also ok!
						         )                                                                                             // make sure that next values are also ok!
						{
							if (!(assignedNormFlags & X_BIT))
							{
								columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_NX);
								assignedNormFlags |= X_BIT; // update bit field accordingly
							}
							else if (!(assignedNormFlags & Y_BIT))
							{
								columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_NY);
								assignedNormFlags |= Y_BIT; // update bit field accordingly
							}
							else if (!(assignedNormFlags & Z_BIT))
							{
								columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_NZ);
								assignedNormFlags |= Z_BIT; // update bit field accordingly
							}
						}
						else
						{
							// maybe it's a scalar?
							columnHeaderWidget->setCurrentIndex(ASCII_OPEN_DLG_Scalar);
						}
					}

					if (columnHeaderWidget->currentIndex() >= 0)
					{
						m_columnType[i] = VALID;
					}
					else
					{
						// we won't look at this column again
						m_columnType[i] = IGNORED;
					}

					columnHeaderWidget->blockSignals(false);
				}
				else
				{
					// we won't look at this column again
					m_columnType[i] = IGNORED;
				}
			}
		}
	}

	m_columnsCount = columnsCount;

	m_ui->tableWidget->setEnabled(true);
	m_ui->buttonWidget->setEnabled(true);

	// check for invalid columns
	checkSelectedColumnsValidity(); // will eventually enable of disable the "OK" button

	m_ui->tableWidget->resizeColumnsToContents();
}

double AsciiOpenDlg::getQuaternionScale() const
{
	return m_ui->quatCSScaleDoubleSpinBox->value();
}

void AsciiOpenDlg::checkSelectedColumnsValidity()
{
	// check for invalid columns
	bool m_selectedInvalidColumns = false;
	{
		assert(m_columnType.size() == static_cast<size_t>(m_columnsCount));
		assert(m_ui->tableWidget->columnCount() >= static_cast<int>(m_columnsCount));
		m_ui->show2DLabelsCheckBox->setEnabled(false);

		unsigned quaternionFields = 0;
		for (unsigned i = 0; i < m_columnsCount; i++)
		{
			QComboBox* columnHeaderWidget = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i));

			if (columnHeaderWidget->currentIndex() == ASCII_OPEN_DLG_Label)
			{
				m_ui->show2DLabelsCheckBox->setEnabled(true);
			}
			else if (m_columnType[i] == TEXT && columnHeaderWidget->currentIndex() != 0)
			{
				// text columns shouldn't be selected (other than for Labels)
				m_selectedInvalidColumns |= true;
			}
			else if (columnHeaderWidget->currentIndex() >= ASCII_OPEN_DLG_QuatW && columnHeaderWidget->currentIndex() <= ASCII_OPEN_DLG_QuatZ)
			{
				++quaternionFields;
			}
		}

		// we only show the 'quaternion' frame if all 4 fields are defined
		m_ui->quaternionFrame->setVisible(quaternionFields >= 4);
	}

	m_ui->applyAllButton->setEnabled(!m_selectedInvalidColumns);
	m_ui->applyButton->setEnabled(!m_selectedInvalidColumns);
}

bool AsciiOpenDlg::CheckOpenSequence(const AsciiOpenDlg::Sequence& sequence, QString& errorMessage)
{
	// two requirements:
	//- at least 2 coordinates must be defined
	//- apart from SFs, only one column assignment per property
	std::vector<unsigned> counters(ASCII_OPEN_DLG_TYPES_COUNT, 0);
	{
		for (const SequenceItem& item : sequence)
		{
			++counters[item.type];
		}
	}

	// check for doublons
	{
		for (size_t i = 1; i < ASCII_OPEN_DLG_Scalar; i++)
		{
			if (counters[i] > 1)
			{
				errorMessage = QString("'%1' defined at least twice!").arg(ASCII_OPEN_DLG_TYPES_NAMES[i]);
				return false;
			}
		}
	}

	unsigned char coordIsDefined[3] = {counters[ASCII_OPEN_DLG_X] != 0,
	                                   counters[ASCII_OPEN_DLG_Y] != 0,
	                                   counters[ASCII_OPEN_DLG_Z] != 0};

	if (coordIsDefined[0] + coordIsDefined[1] + coordIsDefined[2] < 2)
	{
		errorMessage = "At least 2 vertex coordinates must be defined!";
		return false;
	}

	// for quaternions, we need all 4 values
	unsigned quaternionTotal = counters[ASCII_OPEN_DLG_QuatW]
	                           + counters[ASCII_OPEN_DLG_QuatX]
	                           + counters[ASCII_OPEN_DLG_QuatY]
	                           + counters[ASCII_OPEN_DLG_QuatZ];
	if (quaternionTotal != 0 && quaternionTotal != 4)
	{
		errorMessage = "Incomplete quaternion definition! (4 components expected)";
		return false;
	}

	return true;
}

bool AsciiOpenDlg::apply()
{
	QString errorMessage;
	auto    sequence = getOpenSequence();

	if (!CheckOpenSequence(sequence, errorMessage))
	{
		QMessageBox::warning(nullptr, "Error", errorMessage);
		return false;
	}
	else
	{
		// save semi-persistent values
		s_maxCloudSizeDoubleSpinBoxValue = m_ui->maxCloudSizeDoubleSpinBox->value();
		s_csEntitiesScale                = m_ui->quatCSScaleDoubleSpinBox->value();

		if (!s_asciiOpenContext)
		{
			s_asciiOpenContext.reset(new AsciiOpenContext);
		}
		s_asciiOpenContext->save(m_ui);
		s_asciiOpenContext->sequence = sequence;
		s_asciiOpenContext->applyAll = false;

		accept();

		return true;
	}
}

void AsciiOpenDlg::applyAll()
{
	if (apply())
	{
		// backup current open sequence
		if (s_asciiOpenContext)
		{
			s_asciiOpenContext->applyAll = true;
		}
		else
		{
			assert(false);
		}
	}
}

void AsciiOpenDlg::ResetApplyAll()
{
	if (s_asciiOpenContext)
	{
		s_asciiOpenContext->applyAll = false;
	}
}

bool AsciiOpenDlg::restorePreviousContext()
{
	if (!s_asciiOpenContext)
	{
		return false;
	}

	// restore previous dialog state
	s_asciiOpenContext->load(m_ui);
	m_separator = s_asciiOpenContext->separator;
	setSkippedLines(s_asciiOpenContext->skipLines, true);
	updateTable();

	// saved sequence and cloud content don't match!!!
	if (static_cast<size_t>(m_columnsCount) != s_asciiOpenContext->sequence.size())
	{
		s_asciiOpenContext->applyAll = false; // cancel the 'Apply All' effect
		setSkippedLines(0, true);
		autoFindBestSeparator();
		return false;
	}

	// restore columns attributes
	for (unsigned i = 0; i < m_columnsCount; i++)
	{
		QComboBox* combo = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i));
		if (!combo) // yes, it happens if all lines are skipped!
		{
			s_asciiOpenContext->applyAll = false; // cancel the 'Apply All' effect
			return false;
		}
		SequenceItem& item = s_asciiOpenContext->sequence[i];
		combo->setCurrentIndex(item.type);
	}

	QString errorMessage;
	if (!CheckOpenSequence(s_asciiOpenContext->sequence, errorMessage))
	{
		s_asciiOpenContext->applyAll = false; // cancel the 'Apply All' effect
	}

	return true;
}

AsciiOpenDlg::Sequence AsciiOpenDlg::getOpenSequence() const
{
	Sequence seq;

	if (m_columnsCount != 0)
	{
		// shall we extract headerParts?
		QStringList headerParts;
		if (!m_headerLine.isEmpty()
		    && m_ui->extractSFNamesFrom1stLineCheckBox->isEnabled()
		    && m_ui->extractSFNamesFrom1stLineCheckBox->isChecked())
		{
			headerParts = m_headerLine.simplified().split(m_separator);
		}

		seq.reserve(m_columnsCount - 1);
		for (unsigned i = 0; i < m_columnsCount; i++)
		{
			const QComboBox* combo = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i));
			if (!combo) // yes, it happens if all lines are skipped!
				break;
			seq.emplace_back(static_cast<CC_ASCII_OPEN_DLG_TYPES>(combo->currentIndex()), headerParts.size() > static_cast<int>(i) ? headerParts[i] : QString());
		}
	}

	return seq;
}

bool AsciiOpenDlg::safeSequence() const
{
	// less than 6 columns: we probably have a 3D point (3 columns)
	// and 2 other columns (i.e. scalar fields) --> no ambiguity
	if (getColumnsCount() < 6)
		return true;

	// no header
	if (m_headerLine.isEmpty())
		return false;

	AsciiOpenDlg::Sequence seq         = getOpenSequence();
	QStringList            headerParts = m_headerLine.simplified().split(m_separator);

	// not enough column headers?
	if (headerParts.size() < static_cast<int>(seq.size()))
		return false;

	for (int i = 0; i < headerParts.size(); ++i)
	{
		// column header
		QString colHeader = headerParts[i].toUpper();
		// column type
		switch (seq[i].type)
		{
		case ASCII_OPEN_DLG_None:
			break;
		case ASCII_OPEN_DLG_X:
			if (!CouldBeX(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_Y:
			if (!CouldBeY(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_Z:
			if (!CouldBeZ(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_NX:
			if (!CouldBeNx(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_NY:
			if (!CouldBeNy(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_NZ:
			if (!CouldBeNz(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_R:
		case ASCII_OPEN_DLG_Rf:
			if (!CouldBeR(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_G:
		case ASCII_OPEN_DLG_Gf:
			if (!CouldBeG(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_B:
		case ASCII_OPEN_DLG_Bf:
			if (!CouldBeB(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_A:
		case ASCII_OPEN_DLG_Af:
			if (!CouldBeA(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_Grey:
			if (!CouldBeGrey(colHeader)
			    && !colHeader.contains("INT"))
				return false;
			break;
		case ASCII_OPEN_DLG_QuatW:
			if (!CouldBeQuatW(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_QuatX:
			if (!CouldBeQuatX(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_QuatY:
			if (!CouldBeQuatY(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_QuatZ:
			if (!CouldBeQuatZ(colHeader))
				return false;
			break;
		case ASCII_OPEN_DLG_Scalar:
			// a SF name can be anything!
			break;
		case ASCII_OPEN_DLG_RGB32i:
			if (!CouldBeRGBi(colHeader)
			    && !colHeader.contains("RGB"))
				return false;
			break;
		case ASCII_OPEN_DLG_RGB32f:
			if (!CouldBeRGBf(colHeader)
			    && !colHeader.contains("RGB"))
				return false;
			break;
		case ASCII_OPEN_DLG_Label:
			if (!CouldBeLabel(colHeader))
				return false;
			break;
		default:
			// unhandled case?!
			assert(false);
			return false;
		}
	}

	return true;
}

void AsciiOpenDlg::columnsTypeHasChanged(int index)
{
	if (!m_columnsCount)
	{
		return;
	}

	// we get the signal sender
	QObject* obj = sender();
	if (!obj)
	{
		assert(false);
		return;
	}

	// it should be a QComboBox
	QComboBox* changedCombo = qobject_cast<QComboBox*>(obj);
	if (!changedCombo)
	{
		assert(false);
		return;
	}

	// now we look which column's combobox it is
	for (unsigned i = 0; i < m_columnsCount; i++)
	{
		QComboBox* combo = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i));
		// we found the right element
		if (changedCombo == combo)
		{
			if (index == int(ASCII_OPEN_DLG_X) || index == int(ASCII_OPEN_DLG_NX) || index == int(ASCII_OPEN_DLG_R) || index == int(ASCII_OPEN_DLG_Rf) || index == int(ASCII_OPEN_DLG_QuatW))
			{
				// Auto select the next columns type
				if (i + 2 < m_columnsCount)
				{
					QComboBox* nextCombo     = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i + 1));
					QComboBox* nextNextCombo = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i + 2));
					// if the two next columns have no assigned type, we set them auto.
					if (nextCombo->currentIndex() == int(ASCII_OPEN_DLG_None)
					    && nextNextCombo->currentIndex() == int(ASCII_OPEN_DLG_None))
					{
						nextCombo->blockSignals(true);
						nextNextCombo->blockSignals(true);

						if (index == int(ASCII_OPEN_DLG_X))
						{
							nextCombo->setCurrentIndex(ASCII_OPEN_DLG_Y);
							nextNextCombo->setCurrentIndex(ASCII_OPEN_DLG_Z);
						}
						else if (index == int(ASCII_OPEN_DLG_NX))
						{
							nextCombo->setCurrentIndex(ASCII_OPEN_DLG_NY);
							nextNextCombo->setCurrentIndex(ASCII_OPEN_DLG_NZ);
						}
						else if (index == int(ASCII_OPEN_DLG_R))
						{
							nextCombo->setCurrentIndex(ASCII_OPEN_DLG_G);
							nextNextCombo->setCurrentIndex(ASCII_OPEN_DLG_B);
						}
						else if (index == int(ASCII_OPEN_DLG_Rf))
						{
							nextCombo->setCurrentIndex(ASCII_OPEN_DLG_Gf);
							nextNextCombo->setCurrentIndex(ASCII_OPEN_DLG_Bf);
						}
						else if (index == int(ASCII_OPEN_DLG_QuatW))
						{
							if (i + 3 < m_columnsCount) // we need one more value for quaternions
							{
								QComboBox* nexNextNextCombo = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i + 3));
								if (nexNextNextCombo->currentIndex() == int(ASCII_OPEN_DLG_None))
								{
									nextCombo->setCurrentIndex(ASCII_OPEN_DLG_QuatX);
									nextNextCombo->setCurrentIndex(ASCII_OPEN_DLG_QuatY);
									nexNextNextCombo->setCurrentIndex(ASCII_OPEN_DLG_QuatZ);
								}
							}
						}
					}

					nextCombo->blockSignals(false);
					nextNextCombo->blockSignals(false);
				}
			}
		}
		else if (index < ASCII_OPEN_DLG_Scalar) // check that the other combo as the same index (apart from SF)
		{
			if (combo->currentIndex() == index)
			{
				combo->blockSignals(true);
				combo->setCurrentIndex(ASCII_OPEN_DLG_None);
				combo->blockSignals(false);
			}
		}
	}

	checkSelectedColumnsValidity(); // will eventually enable of disable the "OK" button
}

void AsciiOpenDlg::shortcutButtonPressed()
{
	if (!m_columnsCount)
		return;

	// we get the signal sender
	QObject* obj = sender();
	if (!obj)
		return;

	// it should be a QToolButton (could we test this?)
	QToolButton* shortcutButton = static_cast<QToolButton*>(obj);

	char newSeparator = 0;
	if (shortcutButton == m_ui->toolButtonShortcutSpace)
		newSeparator = 32;
	else if (shortcutButton == m_ui->toolButtonShortcutComma)
		newSeparator = 44;
	else if (shortcutButton == m_ui->toolButtonShortcutSemicolon)
		newSeparator = 59;

	if (newSeparator != 0 && getSeparator() != newSeparator)
	{
		setSeparator(QChar(newSeparator));
	}
}

void AsciiOpenDlg::setSeparator(QChar sep)
{
	m_ui->commaDecimalCheckBox->blockSignals(true);
	if (sep == 44) // comma
	{
		m_ui->commaDecimalCheckBox->setEnabled(false);
		// m_ui->commaDecimalCheckBox->setChecked(false);
	}
	else
	{
		m_ui->commaDecimalCheckBox->setEnabled(true);
	}
	m_ui->commaDecimalCheckBox->blockSignals(false);
	m_ui->lineEditSeparator->setText(sep);
}

unsigned AsciiOpenDlg::getMaxCloudSize() const
{
	return static_cast<unsigned>(m_ui->maxCloudSizeDoubleSpinBox->value() * 1.0e6); // static_cast is equivalent to floor if value >= 0
}

bool AsciiOpenDlg::showLabelsIn2D() const
{
	return m_ui->show2DLabelsCheckBox->isEnabled() && m_ui->show2DLabelsCheckBox->isChecked();
}

void AsciiOpenDlg::resetColumnRoles()
{
	// restore columns attributes
	for (unsigned i = 0; i < m_columnsCount; i++)
	{
		QComboBox* combo = static_cast<QComboBox*>(m_ui->tableWidget->cellWidget(0, i));
		if (combo) // yes, it happens if all lines are skipped!
		{
			combo->setCurrentIndex(0);
		}
	}

	checkSelectedColumnsValidity();
}

void AsciiOpenDlg::setSkippedLines(int lineCount, bool blockSignal /*=true*/)
{
	if (lineCount < 0)
	{
		assert(false);
		lineCount = 0;
	}

	if (lineCount == 0)
	{
		m_headerLine.clear();
		m_ui->extractSFNamesFrom1stLineCheckBox->setEnabled(false);
	}

	m_ui->spinBoxSkipLines->blockSignals(blockSignal);
	m_ui->spinBoxSkipLines->setValue(lineCount);
	m_ui->spinBoxSkipLines->blockSignals(false);
}

unsigned AsciiOpenDlg::getSkippedLinesCount() const
{
	return static_cast<unsigned>(std::max(0, m_ui->spinBoxSkipLines->value()));
}
